﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Core.EntityClient;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations;
//using System.Data.Entity.SqlServerCompact;
using System.Data.SqlClient;
using System.Data.SqlServerCe;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;

namespace UnitTestUtils.DB.EF
{
    /// <summary>
    /// Factory for creating <see cref="DbConnection"/>s in the context of EntityFramework. Consecutive calls 
    /// return new/unique databasenames.
    /// </summary>
    public class TestConnectionFactory : IDbConnectionFactory, IDisposable
    {
        private readonly SqlCeConnectionFactory _internalFac;
        
        private readonly object _lock = new object();
        
        private int _currentID;
        private string _dbTemplateName = "TestDB{0}.sdf";
        private readonly string _dbPath;
        private readonly List<ConnectionInfo> _connectionInfos = new List<ConnectionInfo>();
        private readonly ThreadLocal<DbConnection> _threadLocal = new ThreadLocal<DbConnection>();

        /// <summary>
        /// Creates a new factory and allows overriding the location where the db files will get created.
        /// </summary>
        /// <param name="dbpath"></param>
        public TestConnectionFactory(string dbpath = ".\\")
        {
            _dbPath = dbpath;
            _internalFac = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0", _dbPath, "");
            //DbMigrationsConfiguration.AutomaticMigrationsEnabled = true;
        }

        /// <summary>
        /// Use string.Format notation to override the template name, e.g. "TheName{0}.sdf".
        /// </summary>
        public string DBTemplateName
        {
            get { return this._dbTemplateName; }
            set
            {
                if (!value.Contains("{0}"))
                    throw new ArgumentException("Template does not contain '{0}' placeholder for making filename unique!");

                _dbTemplateName = value;
            }
        }

        /// <summary>
        /// Returns the path where the db files will get stored.
        /// </summary>
        public string DBPath
        {
            get { return this._dbPath; }
        }

        /// <summary>
        /// Disposes all created connections.
        /// </summary>
        public void CleanupConnections()
        {
            ConnectionInfo[] infos;
            
            lock (_lock)
            {
                infos = _connectionInfos.ToArray();
                _connectionInfos.Clear();
            }
            foreach (var info in infos)
            {
                try
                {
                    if (info.Connection != null)
                        info.Connection.Dispose();
                }
                catch
                {
                }
                try
                {
                    var filepath = Path.Combine(_dbPath, info.FilePath);
                    if (File.Exists(filepath))
                    {
                        Trace.TraceInformation("TestConnectionFactory: Deleting file {0}", filepath);
                        File.Delete(filepath);    
                    }
                    else
                    {
                        Trace.TraceInformation("TestConnectionFactory: Closed unused connection to db file {0}", filepath);
                    }
                }
                catch
                {
                }
            }
        }

        #region IDbConnectionFactory

        /// <summary>
        /// Creates a connection based on the given database name or connection string.
        /// </summary>
        /// <param name="nameOrConnectionString">The database name or connection string.</param>
        /// <returns>
        /// An initialized DbConnection.
        /// </returns>
        public DbConnection CreateConnection(string nameOrConnectionString)
        {
            if (_threadLocal.IsValueCreated)
            {
                return _threadLocal.Value;
            }

            var dbName = GenerateNewDbName();
            var connection = _internalFac.CreateConnection(dbName);
            RecordConnection(connection,dbName);

            Trace.TraceInformation("TestConnectionFactory: created DbConnection using db name  {0}", dbName);
            return connection;
        }

        /// <summary>
        /// Creates an EntityConnection based on the database name or connection string.
        /// </summary>
        /// <param name="nameOfConnectionString">The database name or connection string.</param>
        /// <returns>An initialized EntityConnection</returns>
        public EntityConnection CreateConnectionEFConnection(string nameOfConnectionString)
        {
            if (_threadLocal.IsValueCreated)
            {
                var efConnection = _threadLocal.Value as EntityConnection;
                if (efConnection != null)
                {
                    return efConnection;
                }
            }

            string dbFilePath;
            var conStr = CreateEFConnectionString_internal(nameOfConnectionString, out dbFilePath);
            var entityConnection = new EntityConnection(conStr);
            RecordConnection(entityConnection, dbFilePath);

            Trace.TraceInformation("TestConnectionFactory: created EntityConnection using db name  {0}", dbFilePath);
            return entityConnection;
        }

        /// <summary>
        /// Creates new connection string based on the configured database name or connection string.
        /// </summary>
        /// <param name="nameOfConnectionString"></param>
        /// <returns></returns>
        public string CreateEntityConnectionString(string nameOfConnectionString)
        {
            string dbFilePath;
            var connectionString = CreateEFConnectionString_internal(nameOfConnectionString, out dbFilePath);

            RecordConnection(null, dbFilePath);

            Trace.TraceInformation("TestConnectionFactory: created DbConnection using db name  {0}", connectionString);
            return connectionString;
        }
        #endregion

        private string CreateEFConnectionString_internal(string nameOfConnectionString, out string dbFilePath)
        {
            var dbName = GenerateNewDbName();

            // load configured connection string from App.config
            string orgConStr =
                System.Configuration.ConfigurationManager.ConnectionStrings[nameOfConnectionString].ConnectionString;
            var builder = new EntityConnectionStringBuilder(orgConStr);
            var provString = builder.ProviderConnectionString;

            // not quite clean, since we assume that SQLCE is used!
            var sqlCeConnectionStringBuilder = new SqlConnectionStringBuilder(provString);
            sqlCeConnectionStringBuilder.DataSource = dbName;

            // update EntityConnection string with the updated data source
            builder.ProviderConnectionString = sqlCeConnectionStringBuilder.ConnectionString;

            dbFilePath = dbName;

            return builder.ConnectionString;
        }

        private string GenerateNewDbName()
        {
            var hashCode = DateTime.Now.GetHashCode();
            string id = string.Format("{0}-{1}", Interlocked.Increment(ref _currentID).ToString(CultureInfo.InvariantCulture), hashCode);
            return string.Format(_dbTemplateName, id);
        }

        private void RecordConnection(DbConnection connection, string filePath)
        {
            _threadLocal.Value = connection;
            var info = new ConnectionInfo(connection, filePath);
            
            lock (_lock)
            {
                _connectionInfos.Add(info);
            }
        }

        #region Implementation of IDisposable
        /// <summary>
        /// <see cref="IDisposable.Dispose"/>
        /// </summary>
        public void Dispose()
        {
            lock (_lock)
            {
                CleanupConnections(); 
            }

        }

        #endregion

        #region ConnectionInfo class

        private class ConnectionInfo
        {
            private readonly string _filePath;
            private readonly DbConnection _connection;

            internal ConnectionInfo(DbConnection connection, string filePath)
            {
                _connection = connection;
                _filePath = filePath;
            }

            public string FilePath
            {
                get { return _filePath; }
            }

            public DbConnection Connection
            {
                get { return _connection; }
            }
        }

        #endregion
    }
}
